from flask import Flask, render_template, request, redirect, url_for, flash, get_flashed_messages, jsonify, session, abort
from flask_login import LoginManager, login_user, logout_user, login_required, current_user, fresh_login_required
from models import db, Expense, User, Category
from collections import defaultdict
from datetime import datetime,timedelta
from flask_migrate import Migrate
from flask_mail import Message
from dateutil import parser
from sqlalchemy.orm import joinedload
import re
from email_validator import validate_email, EmailNotValidError
from sqlalchemy import extract
import os

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///expenses.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = 'mysecretkey'
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1)
# app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=1)
db.init_app(app)
migrate = Migrate(app, db)  # 初始化 Flask-Migrate
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

from flask_mail import Mail

# 邮件服务器配置
app.config['MAIL_SERVER'] = 'smtp.qq.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_TLS'] = False
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = '448349994@qq.com'
app.config['MAIL_PASSWORD'] = 'vbsfzppnaziubheb'  # 确保这是正确的授权码
app.config['MAIL_DEFAULT_SENDER'] = '448349994@qq.com'  # 修改为实际的发件人邮箱
app.config['MAIL_MAX_EMAILS'] = None
app.config['MAIL_ASCII_ATTACHMENTS'] = False

mail = Mail(app)


@login_manager.user_loader
def load_user(user_id):
    return db.session.get(User, int(user_id))

# 用户注册
@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        email = request.form['email']  # 获取用户输入的电子邮件
        password = request.form['password']

        # 检查用户名是否已存在
        if User.query.filter_by(username=username).first():
            flash('用户名已存在，请选择其他用户名。')
            return redirect(url_for('register'))

        # 检查电子邮件是否已存在
        if User.query.filter_by(email=email).first():
            flash('该电子邮件已注册，请使用其他电子邮件。')
            return redirect(url_for('register'))

        # 清除可能存在的其他闪存消息
        get_flashed_messages()

        # 创建新用户并保存
        new_user = User(username=username, email=email)  # 包含电子邮件
        new_user.set_password(password)
        db.session.add(new_user)
        db.session.commit()

        flash('注册成功，请登录。')
        return redirect(url_for('login'))

    return render_template('register.html')

# 用户登录
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        user = User.query.filter_by(username=username).first()

        if user and user.check_password(password):
            login_user(user, remember=False)
            session.permanent = True  # 激活会话永久性
            session['last_active'] = datetime.now().timestamp()  # 设置最后活跃时间
            flash('登录成功！', 'success')
            # 清除可能存在的其他闪存消息
            get_flashed_messages()

            # 重定向到用户登录前尝试访问的页面，或者重定向到首页
            next_page = request.args.get('next')
            return redirect(next_page or url_for('home'))
        else:
            flash('用户名或密码错误。', 'danger')

    return render_template('login.html')

# 检查会话是否超时
@app.before_request
def check_session_timeout():
    if current_user.is_authenticated:
        now = datetime.now().timestamp()
        last_active = session.get('last_active', now)

        # 检查是否超时
        if now - last_active > 3600:  # 超过1小时
            flash('登录已过期，请重新登录', 'warning')
            logout_user()
            return redirect(url_for('login'))

        # 更新最后活动时间
        session['last_active'] = now

# 忘记密码
@app.route('/forgot_password', methods=['GET', 'POST'])
def forgot_password():
    if request.method == 'POST':
        username = request.form.get('username')
        print(f"处理用户 {username} 的密码重置请求")
        user = User.query.filter_by(username=username).first()
        
        if user:
            try:
                # 测试邮件连接
                with mail.connect() as conn:
                    token = user.get_reset_token()
                    reset_url = url_for('reset_password', token=token, _external=True)
                    
                    msg = Message(
                        subject='重置密码请求',
                        recipients=[user.email],
                        body=f'''请点击以下链接重置您的密码：
{reset_url}

如果你没有发出此请求，请忽略此邮件。
''',
                        sender=('Your App Name', app.config['MAIL_USERNAME'])
                    )
                    
                    conn.send(msg)
                    print(f"邮件已成功发送至 {user.email}")
                    flash('密码重置链接已发送至您的邮箱。', 'success')
                    
            except Exception as e:
                print(f"发送邮件时出错: {str(e)}")
                flash(f'发送邮件失败: {str(e)}', 'error')
                return redirect(url_for('forgot_password'))
                
            masked_email = mask_email(user.email)
            return render_template('forgot_password.html', username=username, masked_email=masked_email)
        else:
            flash("用户名不存在，请检查后重试。", "danger")
            
    return render_template('forgot_password.html')

# 用户登出
@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('login'))

# 获取本地时间
# def get_local_time(utc_dt):
#     # 设置你的时区，比如亚洲上海
#     local_tz = timezone('Asia/Shanghai')
#     return utc_dt.astimezone(local_tz)
#
# def utc_to_local(utc_time):
#     # 假设本地时区为东八区，可以根据你的时区调整
#     # local_timezone = timezone(timedelta(hours=+8))
#     # utc_dt = datetime.fromisoformat(utc_time)
#     # return utc_dt.astimezone(local_timezone).strftime('%Y-%m-%d %H:%M:%S')
#     if not utc_time:  # 如果时间为空或无效
#         return "无效时间"  # 你可以选择返回任何其他默认值
#     try:
#         utc_dt = datetime.fromisoformat(utc_time)  # 解析 UTC 时间
#         local_dt = utc_dt.astimezone()  # 转换为本地时间
#         return local_dt.strftime('%Y-%m-%d %H:%M:%S')  # 返回格式化后的本地时间
#     except ValueError as e:
#         print(f"时间格式解析错误: {str(e)}")
#         return "时间解析错误"  # 如果时间格式错误，返回提示

# 首页，显示用户消费记录和预算设置
@app.route('/')
@login_required
def home():
    expenses = Expense.query.filter_by(user_id=current_user.id).all()
    # local_timezone = timezone(timedelta(hours=+8))

    # 将费用列表格式化
    expenses_list = [
        {
            "id": expense.id,
            "date": expense.date.strftime('%Y-%m-%d'),  # 日期格式化为字符串，适合展示
            "description": expense.description,
            "amount": expense.amount,
            "payment_method": expense.payment_method,
            "category": expense.category.name
        }
        for expense in expenses
    ]

    # 按分类聚合消费金额
    category_totals = defaultdict(float)
    for expense in expenses:
        category_totals[expense.category.name] += expense.amount

    # 将聚合后的数据转换为适合前端使用的格式
    categories = list(category_totals.keys())
    amounts = list(category_totals.values())

    return render_template(
        'index.html',
        expenses=expenses,
        expenses_list=expenses_list,
        categories=categories,
        amounts=amounts
    )

# 设置预算
@app.route('/set_budget', methods=['POST'])
@login_required
def set_budget():
    budget = request.form.get('budget')
    if budget:
        current_user.monthly_budget = float(budget)
        db.session.commit()
    return redirect(url_for('home'))

# 添加消费记录
@app.route('/add', methods=['POST'])
@login_required
def add_expense():
    description = request.form.get('description')
    amount = request.form.get('amount')
    payment_method = request.form.get('payment_method')
    category_id = request.form.get('category')  # 获取分类ID
    selected_date = request.form.get('date')  # 获取前端传递的本地时间

    # 检查所有字段是否都有值
    if not description or not amount or not payment_method or not category_id:
        flash('所有字段都是必填项，请完整填写表单。', 'danger')
        return redirect(url_for('add_expense_page'))

    try:
        # 将用户选择的日期转换为 datetime 对象
        expense_date = datetime.strptime(selected_date, '%Y-%m-%d').date()
        new_expense = Expense(
            description=description,
            amount=float(amount),  # 将金额转换为浮点数
            payment_method=payment_method,
            category_id=int(category_id),  # 将分类ID转换为整数
            user_id=current_user.id,
            date=expense_date  # 使用用户传递的本地时间保存
        )
        db.session.add(new_expense)
        db.session.commit()
        flash('消费记录添加成功！', 'success')
    except ValueError:
        flash('金额或分类ID无效，请检查输入内容。', 'danger')
    except Exception as e:
        db.session.rollback()  # 如果发生错误，回滚事务
        flash(f'发生错误：{str(e)}', 'danger')

    return redirect(url_for('home'))

@app.route('/edit/<int:id>', methods=['GET', 'POST'])
@login_required
def edit_expense(id):
    expense = Expense.query.get_or_404(id)

    if expense.user_id != current_user.id:
        flash('无权编辑该记录。')
        return redirect(url_for('home'))

    if request.method == 'POST':
        expense.description = request.form.get('description')
        expense.amount = float(request.form.get('amount'))
        expense.payment_method = request.form.get('payment_method')
        category_id = request.form.get('category')

        if category_id:
            expense.category_id = int(category_id)

        db.session.commit()
        return redirect(url_for('home'))

    categories = Category.query.filter_by(user_id=current_user.id).all()
    return render_template('edit.html', expense=expense, categories=categories)

# 删除消费记录
@app.route('/delete/<int:id>', methods=['GET', 'POST'])
@login_required
def delete_expense(id):
    expense = Expense.query.get_or_404(id)

    if expense.user_id != current_user.id:
        flash('无权删除该记录。')
        return redirect(url_for('home'))

    db.session.delete(expense)
    db.session.commit()
    return redirect(url_for('view_expenses'))

# 管理分类
@app.route('/categories', methods=['GET', 'POST'])
@login_required
def manage_categories():
    if request.method == 'POST':
        category_name = request.form.get('category_name')
        if category_name:
            new_category = Category(name=category_name, user_id=current_user.id)
            db.session.add(new_category)
            db.session.commit()
            flash('分类已添加')

    categories = Category.query.filter_by(user_id=current_user.id).all()
    return render_template('manage_categories.html', categories=categories)

# 删除分类
@app.route('/delete_category/<int:category_id>', methods=['POST'])
@login_required
def delete_category(category_id):
    category = Category.query.get_or_404(category_id)

    # 检查是否有与该分类关联的消费记录
    expenses_with_category = Expense.query.filter_by(category_id=category.id).count()

    if expenses_with_category > 0:
        flash('无法删除该分类，因为已有消费记录与其关联。', 'warning')
        return redirect(url_for('manage_categories'))

    # 删除分类
    db.session.delete(category)
    db.session.commit()
    flash('分类删除成功。', 'success')
    return redirect(url_for('manage_categories'))

@app.route('/')
@login_required
def index():
    return render_template('index.html')

@app.route('/add')
@login_required
def add_expense_page():
    return render_template('add_expense_page.html')

@app.route('/expenses')
@login_required
def view_expenses():
    # 获取用户输入的年份和月份
    selected_year = request.args.get('year', datetime.now().year, type=int)
    selected_month = request.args.get('month', datetime.now().month, type=int)

    # 根据选择的年份和月份筛选消费记录
    expenses_query = Expense.query.filter(Expense.user_id == current_user.id)
    if selected_month != 0:
        expenses_query = expenses_query.filter(
            extract('year', Expense.date) == selected_year,
            extract('month', Expense.date) == selected_month
        )
    else:
        expenses_query = expenses_query.filter(
            extract('year', Expense.date) == selected_year
        )

    # 获取筛选后消费记录的总金额
    total_spent = expenses_query.with_entities(db.func.sum(Expense.amount)).scalar() or 0

    # 翻页功能
    page = request.args.get('page', 1, type=int)  # 获取当前页数
    per_page = 20  # 每页显示10条记录
    # 预加载 category 以避免 N+1 查询问题
    expenses_pagination = expenses_query.options(joinedload(Expense.category)).order_by(Expense.date).paginate(page=page, per_page=per_page, error_out=False)
    expenses_list = []
    for expense in expenses_pagination.items:
        expenses_list.append({
            "id": expense.id,
            "date": expense.date.strftime('%Y-%m-%d'),  # 使用格式化后的日期
            "description": expense.description,
            "amount": expense.amount,
            "payment_method": expense.payment_method,
            "category": expense.category.name
        })

    return render_template('view_expenses.html',
                           expenses=expenses_list,
                           pagination=expenses_pagination,
                           total_spent=total_spent,
                           selected_year=selected_year,
                           selected_month=selected_month,
                           datetime=datetime    # 传递 datetime 模块给模板
                           )

@app.route('/charts')
@login_required
def view_charts():
    # 获取用户输入的年份和月份，默认为当前年月
    selected_year = request.args.get('year', datetime.now().year, type=int)
    selected_month = request.args.get('month', datetime.now().month, type=int)

    # 构建查询
    expenses_query = Expense.query.filter(Expense.user_id == current_user.id)
    
    # 根据选择的年份和月份筛选消费记录
    if selected_month != 0:  # 如果选择了具体月份
        expenses_query = expenses_query.filter(
            extract('year', Expense.date) == selected_year,
            extract('month', Expense.date) == selected_month
        )
    else:  # 如果只选择了年份
        expenses_query = expenses_query.filter(
            extract('year', Expense.date) == selected_year
        )

    # 获取筛选后的消费记录
    expenses = expenses_query.all()

    # 使用 defaultdict 进行按分类汇总
    category_totals = defaultdict(float)
    for expense in expenses:
        category_totals[expense.category.name] += expense.amount

    # 计算总支出
    total_spent = sum(category_totals.values())

    # 将字典数据转换为适合传递给模板的格式
    categories = list(category_totals.keys())  # 分类名称
    amounts = list(category_totals.values())  # 分类对应的消费总额

    # 将数据传递给模板
    return render_template('view_charts.html',
                         categories=categories,
                         amounts=amounts,
                         selected_year=selected_year,
                         selected_month=selected_month,
                         total_spent=total_spent,
                         datetime=datetime)  # 传递 datetime 模块给模板

@app.route('/send_reset_email', methods=['POST'])
def send_reset_email():
    username = request.form['username']
    user = User.query.filter_by(username=username).first()

    if user and user.email:
        token = user.get_reset_token()  # 假设你有生成token的方法
        msg = Message('重置密码请求',
                      sender='noreply@demo.com',
                      recipients=[user.email])
        msg.body = f'''请点击以下链接重置您的密码：
{url_for('reset_password', token=token, _external=True)}
如果你没有发出此请求，请忽略此邮件。
'''
        mail.send(msg)
        flash('密码重置链接已发送至您的邮箱。', 'info')
        return redirect(url_for('login'))

    flash('该用户不存在或没有邮箱地址', 'warning')
    return redirect(url_for('forgot_password'))

@app.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
    user = User.verify_reset_token(token)
    if not user:
        flash('该重置链接无效或已过期。', 'warning')
        return redirect(url_for('forgot_password'))

    if request.method == 'POST':
        password = request.form.get('password')
        if password:
            user.set_password(password)
            db.session.commit()
            flash('您的密码已更新，请登录。', 'success')
            return redirect(url_for('login'))
        else:
            flash('请输入有效的密码。', 'danger')

    return render_template('reset_password.html')

@app.context_processor
def utility_processor():
    return dict(parser=parser)

@app.route('/profile', methods=['GET', 'POST'])
@fresh_login_required
def profile():
    if request.method == 'POST':
        email = request.form.get('email', '').strip()
        new_password = request.form.get('password', '').strip()
        confirm_password = request.form.get('confirm_password', '').strip()
        # 记录更新标志
        data_changed = False
        # print(email, new_password, confirm_password)

        # 更新邮箱
        if email and email != current_user.email:
            if myvalidate_email(email):
                current_user.email = email
                data_changed = True
                # print(data_changed)
            else:
                flash('邮箱格式不正确，请重新输入', 'danger')
                return redirect(url_for('profile'))

        # 更新密码
        if new_password:
            # 验证密码是否符合规则
            if new_password != confirm_password:
                flash('新密码和确认密码不一致，请重新输入', 'danger')
                return redirect(url_for('profile'))

            if not validate_password(new_password):
                flash('密码必须至少8位，且包含大写字母、小写字母、数字或特殊符号中的任意两种', 'danger')
                return redirect(url_for('profile'))

            # 更新密码
            current_user.set_password(new_password)
            data_changed = True

        # 提交更改到数据库
        if data_changed:
            try:
                db.session.commit()
                # print(current_user.id, current_user.email)
                flash('个人资料更新成功！', 'success')
            except Exception as e:
                db.session.rollback()
                flash(f'更新失败: {str(e)}', 'danger')
        else:
            print(current_user.id, current_user.email)
            flash('未检测到任何更改', 'info')  # 没有检测到数据更改的反馈
        return redirect(url_for('profile'))

    return render_template('profile.html', user=current_user)

@app.route('/get_email', methods=['POST'])
def get_email():
    username = request.json.get('username')

    # 查询用户
    user = User.query.filter_by(username=username).first()

    if user:
        # 获取用户邮箱并进行半脱敏处理
        email = mask_email(user.email)
        return jsonify({'email': email}), 200
    else:
        # 如果用户不存在，返回空值
        return jsonify({'email': ''}), 404

def validate_password(password):
    """验证密码至少包含大写字母、小写字母、数字或特殊符号中的两种"""
    if len(password) < 8:
        return False
    categories = 0
    if re.search(r'[A-Z]', password):  # 大写字母
        categories += 1
    if re.search(r'[a-z]', password):  # 小写字母
        categories += 1
    if re.search(r'[0-9]', password):  # 数字
        categories += 1
    if re.search(r'[!@#$%^&*(),.?":{}|<>]', password):  # 特殊符号
        categories += 1
    return categories >= 2

def myvalidate_email(email):
    try:
        # 验证邮箱格式是否正确
        validate_email(email)
        return True
    except EmailNotValidError:
        return False

def mask_email(email):
    """将邮箱中间部分替换为星号，进行半脱敏"""
    local_part, domain = email.split('@')
    masked_local_part = local_part[0] + '*' * (len(local_part) - 2) + local_part[-1]
    return masked_local_part + '@' + domain


if __name__ == '__main__':
    with app.app_context():
        try:
            db.create_all()  # 创建所有数据库表
            # 检查是否需要进行数据库迁移
            if not os.path.exists('migrations'):
                migrate.init()  # 初始化迁移
            migrate.migrate()  # 执行迁移
        except Exception as e:
            print(f"Database initialization error: {e}")
    app.run(debug=True)
